﻿using System;
using System.Linq;
using System.Text;
using System.Collections.Generic;
using System.IO;
using Yaml.Presentation.Tokens;
using Yaml.Presentation.Parsing.TokenScanners;

namespace Yaml.Presentation.Parsing {
    /// <summary>
    /// This class is responsible for scanning the input characters ("stream")
    /// and generating a list of tokens.  At this point there is some simple
    /// syntax checking but no semantic checking.  That is done by the EventGenerator.
    /// </summary>
    public class Scanner {

        #region Private Fields
        /// <summary>
        /// The "stream" of characters we are reading from
        /// </summary>
        private Reader reader;
        /// <summary>
        /// The current queue of tokens found during scanning
        /// </summary>
        private TokenQueue tokens;
        /// <summary>
        /// The collection of scanners that generate the tokens
        /// </summary>
        private TokenScanner[] scanners;
        /// <summary>
        /// The state of the scanning process
        /// </summary>
        private ScannerState state;
        #endregion

        #region Construction
        /// <summary>
        /// Create a new YAML scanner.
        /// </summary>
        /// <param name="reader">The input text</param>
        public Scanner(Reader reader) {
            this.reader = reader;
            this.tokens = new TokenQueue();
            state = new ScannerState();

            InitializeScanners();

            AddStreamStartToken();
        }

        private void InitializeScanners() {
            scanners = new TokenScanner[] {
                new StreamEndTokenScanner(this),
                new DirectiveTokenScanner(this),
                new DocumentStartTokenScanner(this),
                new DocumentEndTokenScanner(this),
                new FlowSequenceStartTokenScanner(this),
                new FlowSequenceEndTokenScanner(this),
                new FlowMappingStartTokenScanner(this),
                new FlowMappingEndTokenScanner(this),
                new FlowEntryTokenScanner(this),
                new BlockEntryTokenScanner(this),
                new MappingKeyTokenScanner(this),
                new MappingValueTokenScanner(this),
                new AliasTokenScanner(this),
                new AnchorTokenScanner(this),
                new TagTokenScanner(this),
            };
        }
        #endregion

        #region Properties
        public Reader Reader { get { return this.reader; } }
        public ScannerState State { get { return this.state; } }
        public TokenQueue Tokens { get { return this.tokens; } }
        #endregion

        #region Tokens
        /// <summary>
        /// Return the next token but do not remove it from the token queue
        /// </summary>
        /// <returns>The first token in the queue</returns>
        public Token PeekToken() {
            FetchMoreTokensAsNeeded();
            return tokens.Peek();
        }

        /// <summary>
        /// Remove and return the next token from the queue.
        /// </summary>
        /// <returns>The first token in the queue</returns>
        public Token GetToken() {
            FetchMoreTokensAsNeeded();
            return tokens.Pop();
        }

        /// <summary>
        /// Do we need to scan for more tokens?
        /// </summary>
        /// <returns>True if we do need to scan for more</returns>
        private bool NeedMoreTokens() {
            if (State.Done) {
                return false;
            }
            if (tokens.Count == 0) {
                return true;
            }
            // Even if we have tokens we may need to look for simple key tokens
            State.SimpleKeys.RemoveInvalidSimpleKeys(GetMark());
            if (State.SimpleKeys.GetNearestSimpleKey() == null) {
                return false;
            }
            return (State.SimpleKeys.GetNearestSimpleKey().TokenNumber == tokens.TokensTaken);
        }

        private void FetchMoreTokensAsNeeded() {
            while (NeedMoreTokens()) {
                FetchMoreTokens();
            }
        }

        private void FetchMoreTokens() {
            FindNextToken();
            Mark currentPosition = GetMark();
            State.SimpleKeys.RemoveInvalidSimpleKeys(currentPosition);
            UnwindIndent(currentPosition);
            foreach (TokenScanners.TokenScanner scanner in scanners) {
                Token token = scanner.TryFetchToken();
                if (token != null) {
                    tokens.Push(token);
                    return;
                }
            }
            throw new ScannerException("while scanning for the next token, found token '" + Reader.Peek() + "' that cannot start any token " + currentPosition.ToString());
        }

        private void FindNextToken() {
            bool found = false;
            while (!found) {
                if (SkipComment()) {
                    if (State.NotInFlow) {
                        State.SimpleKeys.IsSimpleKeyAllowed = true;
                    }
                } else {
                    found = true;
                }
            }
        }


        #endregion

        #region Blocks
        /// <summary>
        /// Unwind the indentation pushing block end tokens into the token queue accordingly.
        /// </summary>
        /// <param name="currentPosition"></param>
        internal void UnwindIndent(Mark currentPosition) {
            // NON_CONFORMING: Ignore indentation within flow context
            if (State.NotInFlow) {
                while (State.Indentation.Current > currentPosition.Column) {
                    State.Indentation.Pop();
                    tokens.Push(new Tokens.BlockEndToken(currentPosition, currentPosition, State.Indentation.Current));
                }
            }
        }

        internal void UnwindAllIndent(Mark currentPosition) {
            while (State.Indentation.Current > -1) {
                State.Indentation.Pop();
                tokens.Push(new Tokens.BlockEndToken(currentPosition, currentPosition, State.Indentation.Current));
            }
        }

        #endregion

        /// <summary>
        /// Create a mark object for the current position.
        /// </summary>
        public Mark GetMark() {
            return new Mark(reader.Index, state.Line, state.Column);
        }

        /// <summary>
        /// Move forward the specified number of characters, treating CR-LF as a single character.
        /// </summary>
        /// <param name="count"></param>
        public void Forward(int count) {
            Forward(count, true);
        }

        /// <summary>
        /// Move forward the specified number of characters.
        /// </summary>
        /// <param name="count">The number of characters to move forward.</param>
        /// <param name="normalizeLineBreaks">If true then CR-LF pair is counted as a single character</param>
        public void Forward(int count, bool normalizeLineBreaks) {
            while (count > 0 && !Reader.AtEnd) {
                count--;
                char ch = Reader.Read();
                if (Scanner.IsLineBreakChar(ch)) {
                    State.NewLine();
                    if (normalizeLineBreaks) {
                        if (ch == '\r' && Reader.Peek() == '\n') {
                            Reader.Forward();
                        }
                    }
                } else {
                    State.NewColumn();
                }
            }
        }

        public void SkipToLineBreak() {
            char ch = Reader.Peek();
            while (!Scanner.IsLineBreakOrNullChar(ch)) {
                Forward(1);
                ch = Reader.Peek();
            }
        }

        public void SkipLineBreaks() {
            while (Scanner.IsLineBreakChar(Reader.Peek())) {
                Forward(1);
            }
        }

        public void SkipSpaces() {
            while (Reader.Peek() == ' ') {
                Forward(1);
            }
        }

        public void SkipSpaceAndLineBreaks() {
            while (Reader.Peek() == ' ' || Scanner.IsLineBreakChar(Reader.Peek())) {
                Forward(1);
            }
        }

        /// <summary>
        /// Skip characters that constitute a comment -
        /// this includes space characters before the # mark and extra empty lines after the # mark.
        ///    l-comment ::= s-ignored-space* c-b-comment
        ///    c-b-comment ::= c-nb-comment-text? b-ignored-any
        ///    c-nb-comment-text ::= "#" nb-char*
        /// </summary>
        public bool SkipComment() {
            bool foundComment = false;
            SkipSpaces();
            if (Reader.Peek() == '#') {
                foundComment = true;
                SkipToLineBreak();
            }
            SkipSpaceAndLineBreaks();
            return foundComment;
        }

        private void AddStreamStartToken() {
            Mark m = GetMark();
            tokens.Push(new Tokens.StreamStartToken(m,m, Reader.Encoding));
            State.SimpleKeys.IsSimpleKeyAllowed = true;
        }

        /// <summary>
        /// Check whether the next token in the queue is of the given type.
        /// </summary>
        /// <param name="choices">An array of types to check against</param>
        /// <returns>True if the next token is one of the specified types.</returns>
        private bool CheckToken(params Type[] choices) {
            FetchMoreTokensAsNeeded();
            if (tokens.Count > 0) {
                if (choices.Length == 0) {
                    return true;
                }
                Token topToken = tokens.Peek();
                foreach (Type choice in choices) {
                    if (choice.IsInstanceOfType(topToken)) {
                        return true;
                    }
                }
            }
            return false;
        }

        public void TransformLineEnd() {
            throw new NotImplementedException();
        }

        internal static void Warning(string p) {
            //TODO: Create warning stuff.
        }

        static public bool IsAlphanumeric(char ch) {
            return ('0' <= ch && ch <= '9') || ('A' <= ch && ch <= 'Z') || ('a' <= ch && ch <= 'z') || ch == '_' || ch == '-';
        }

        static public bool IsDigit(char ch) {
            return '0' <= ch && ch <= '9';
        }

        static public bool IsHexDigit(char ch) {
            return ('0' <= ch && ch <= '9') || ('A' <= ch && ch <= 'F') || ('a' <= ch && ch <= 'f');
        }

        static public bool IsLineBreakChar(char ch) {
            return "\r\n\x85\u2028\u2029".Contains(ch);
        }
        static public bool IsLineBreakOrNullChar(char ch) {
            return "\0\r\n\x85\u2028\u2029".Contains(ch);
        }
        static public bool IsSpaceLineBreakOrNullChar(char ch) {
            return " \r\n\x85\u2028\u2029\0".Contains(ch);
        }
        static public bool IsUriChar(char ch) {
            return IsAlphanumeric(ch) || "-;/?:@&=+$,_.!~*\'()[]%".Contains(ch);
        }
    }
}
